package com.atguigu.gulimall.product.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.atguigu.gulimall.product.service.CategoryBrandRelationService;
import com.atguigu.gulimall.product.vo.Catelog2Vo;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.atguigu.common.utils.PageUtils;
import com.atguigu.common.utils.Query;

import com.atguigu.gulimall.product.dao.CategoryDao;
import com.atguigu.gulimall.product.entity.CategoryEntity;
import com.atguigu.gulimall.product.service.CategoryService;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;


@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {

//    @Autowired
//    CategoryDao categoryDao;

    @Autowired
    CategoryBrandRelationService categoryBrandRelationService;

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private RedissonClient redisson;

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<CategoryEntity>()
        );

        return new PageUtils(page);
    }

    /**
     * 查询所有分类数据,并以树形分类返回
     **/
    @Override
    public List<CategoryEntity> listWithTree() {
        //1: 查询所有分类
        List<CategoryEntity> entityList = baseMapper.selectList(null);
        //2: 对所有分类进行组装成树状并返回
        //2.1:找到所有的一级分类
        List<CategoryEntity> parentList = entityList.stream().filter(categoryEntity ->
                categoryEntity.getParentCid() == 0
        ).map(menu -> {
            //进行映射,收集子菜单
            menu.setChildren(getChildrens(menu, entityList));
            return menu;
        }).sorted((menu1, menu2) -> {
            //对菜单进行排序
            return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
        }).collect(Collectors.toList());
        return parentList;
    }

    /**
     * 递归查询所有子菜单数据
     **/
    private List<CategoryEntity> getChildrens(CategoryEntity parent, List<CategoryEntity> entityList) {
        List<CategoryEntity> categoryEntityList = entityList.stream().filter(categoryEntity -> {
            return categoryEntity.getParentCid().equals(parent.getCatId());
        }).map(categoryEntity -> {
            //收集子菜单下可能还有的子菜单
            categoryEntity.setChildren(getChildrens(categoryEntity, entityList));
            return categoryEntity;
        }).sorted((menu1, menu2) -> {
            //进行排序
            return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
        }).collect(Collectors.toList());
        return categoryEntityList;
    }

    @Override
    public void removeMenuByIds(List<Long> asList) {

        //TODO 1.检查当前删除的菜单,是否被别的地方引用
        baseMapper.deleteBatchIds(asList);

    }

    //[2,25,225]
    @Override
    public Long[] findCatelogPath(Long catelogId, List<Long> list) {

        List<Long> parentPath = findParentPath(catelogId, list);
        Collections.reverse(parentPath);
        return (Long[]) parentPath.toArray();
    }

    @Override
    public Long[] findCatelogPath(Long catelogId) {
        List<Long> list = new ArrayList();
        list.add(catelogId);
        CategoryEntity byId = this.getById(catelogId);
        if (byId.getParentCid() != 0) {
            //收集当前节点ID
            list.add(catelogId);
            findCatelogPath(byId.getParentCid(), list);
        }
        return list.toArray(new Long[list.size()]);
    }

    private List<Long> findParentPath(Long catelogId, List<Long> list) {
        list.add(catelogId);
        CategoryEntity byId = this.getById(catelogId);
        if (byId.getParentCid() != 0) {
            //收集当前节点ID
            list.add(catelogId);
            findCatelogPath(byId.getParentCid(), list);
        }
        return list;
    }


    /**
     * 级联更新所有关联的数据
     *
     * @param category
     */
    @Caching(evict = {
            @CacheEvict(value = "catelogJson", key = "'getLevel1Categorys'"),
            @CacheEvict(value = {"catelogJson"}, key = "'getCatelogJson'")

    })
    @Transactional
    @Override
    public void updateCascade(CategoryEntity category) {
        this.updateById(category);
        categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
    }

    /**
     * @Cacheable注解代表当前方法需要缓存,如果缓存中有,方法不需要调用,如果没命中缓存,会调用方法,最后将方法执行结果放入缓存 每一个需要缓存的数据我们都要指定要方法哪个名字的缓存(缓存的分区按照业务类型来区分)
     * <p>
     * springCache框架保存数据的默认行为:
     * 1:如果缓存中有,方法不调用
     * 2: key默认自动生成,缓存的名字::SimpleKey{}(这个是自动生成的)
     * 3: 缓存的value的值,默认使用JDK序列化机制,将序列化后的数据存到Redis
     * 4: 默认ttl缓存过期时间=-1,永不过期
     * 我们需要自定义的事:
     * 1: 指定生成的缓存使用的key : 根据@cache注解中的key属性指定,
     * 2: 指定缓存的数据的存活时间 : 在配置文件中配置修改spring.cache.redis.time-to-live=36000000
     * 3: 将数据保存的时候是json格式
     **/
    @Cacheable(value = {"catelogJson"}, key = "@root.method.name")
    @Override
    public List<CategoryEntity> getLevel1Categorys() {

        List<CategoryEntity> entityList = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));

        return entityList;
    }


    //TODO Redis缓存，会导致堆外内存溢出：outOfDirectMemoryError
    //1: sprtingBoot2.0以后默认使用Lettuce作为操作Redis的客户端，它使用netty直接进行网络通讯，使用的是直接内存，而不是堆内存
    //2: Lettuce的bug导致netty的堆外内存溢出：原因-》配置-Xmx100m的时候，netty如果没有指定堆外内存，默认使用-Xmx的全部内存
    //解决方案: 不能使用-Dio.netty.maxDirectory只去调大堆外内存
    //1: 升级Lettuce客户端,去自己封装它的源码 2: 切换成jedis做为Redis客户端
    @Cacheable(value = "catelogJson",key = "#root.methodName")
    @Override
    public Map<String, List<Catelog2Vo>> getCatelogJson() {
/**
 * 将数据库的多次查询变为一次
 **/
        List<CategoryEntity> categoryEntityList = baseMapper.selectList(null);

        //查询所有的以及分类ID
        List<CategoryEntity> level1Categorys = getParent_cid(categoryEntityList, 0L);
        Map<String, List<Catelog2Vo>> parentVo = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            List<CategoryEntity> selectList = getParent_cid(categoryEntityList, v.getCatId());

            //封装上面的结果
            List<Catelog2Vo> collect = null;
            if (!CollectionUtils.isEmpty(selectList)) {
                collect = selectList.stream().map(l2 -> {
                    Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
                    //找出当前二级分类的三级分类数据封装成vo
                    List<CategoryEntity> parentId = getParent_cid(categoryEntityList, l2.getCatId());
                    if (!CollectionUtils.isEmpty(parentId)) {
                        List<Catelog2Vo.Catelog3Vo> voList = parentId.stream().map(l3 -> {
                            //封装成指定格式
                            Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                            return catelog3Vo;
                        }).collect(Collectors.toList());
                        catelog2Vo.setCatelog3list(voList);
                    }

                    return catelog2Vo;
                }).collect(Collectors.toList());
            }
            return collect;
        }));
        return parentVo;
    }


    public Map<String, List<Catelog2Vo>> getCatelogJson2() {
        //给缓存中存取数据的时候要存json数据，拿出的时候也是json数据，所以要逆转成我们指定的对象类型（也就是我们说的序列化与反序列化）
        //1：加入缓存逻辑。查询缓存中是否有数据，命中就直接返回，没命中，再去数据库查询，并且将查询到的数据以json格式存入Redis中
        //为什么要存json格式呢，因为别的语言来取出数据的时候json，大家都能直接用

        /**
         * 解决缓存穿透: 空结果缓存
         * 解决缓存雪崩: 设置过期时间(加随机数)
         * 解决缓存击穿: 查询数据库的时候枷锁
         **/
        String catelogJson = redisTemplate.opsForValue().get("catelogJson");
        if (StringUtils.isEmpty(catelogJson)) {
            //从数据库中查询数据
            Map<String, List<Catelog2Vo>> catelogJsonFromDb = getCatelogJsonFromDb();
            return catelogJsonFromDb;
        }

        //如果Redis中有数据，将从Redis获取到的数据转成我们指定的数据格式，再返回
        Map<String, List<Catelog2Vo>> result = JSON.parseObject(catelogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
        });
        return result;
    }

    /**
     * 用redisson做分布式锁
     **/
    public Map<String, List<Catelog2Vo>> getCatelogJsonFromDbForRedisson() {

        /**
         * 占用分布式锁
         * 锁的命名也很重要,实际开发中,锁粒度越细,查询越快
         * 锁命名约束: 比如商品,应该是product + 商品ID + lock
         **/
        RLock redissonLock = redisson.getLock("catelogJson");
        redissonLock.lock();
        /**
         * 只要是同一把锁,就能锁住需要这个锁的所有线程
         * 线程得到锁之后,还要去Redis查询一下数据,如果有就直接返回,没有了,再去查询数据库
         **/
        Map<String, List<Catelog2Vo>> result;
        try {
            String catelogJson = redisTemplate.opsForValue().get("catelogJson");
            if (StringUtils.isEmpty(catelogJson)) {
                /**
                 * 将数据库的多次查询变为一次
                 **/
                List<CategoryEntity> categoryEntityList = baseMapper.selectList(null);

                //查询所有的以及分类ID
                List<CategoryEntity> level1Categorys = getParent_cid(categoryEntityList, 0L);
                Map<String, List<Catelog2Vo>> parentVo = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
                    List<CategoryEntity> selectList = getParent_cid(categoryEntityList, v.getCatId());

                    //封装上面的结果
                    List<Catelog2Vo> collect = null;
                    if (!CollectionUtils.isEmpty(selectList)) {
                        collect = selectList.stream().map(l2 -> {
                            Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
                            //找出当前二级分类的三级分类数据封装成vo
                            List<CategoryEntity> parentId = getParent_cid(categoryEntityList, l2.getCatId());
                            if (!CollectionUtils.isEmpty(parentId)) {
                                List<Catelog2Vo.Catelog3Vo> voList = parentId.stream().map(l3 -> {
                                    //封装成指定格式
                                    Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                                    return catelog3Vo;
                                }).collect(Collectors.toList());
                                catelog2Vo.setCatelog3list(voList);
                            }

                            return catelog2Vo;
                        }).collect(Collectors.toList());
                    }
                    return collect;
                }));
                String data = JSON.toJSONString(parentVo);
                redisTemplate.opsForValue().set("catelogJson", data, 1, TimeUnit.DAYS);
                return parentVo;
            }
            result = JSON.parseObject(catelogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
            });
            //将数据转成json格式，并存入Redis中
            String data = JSON.toJSONString(result);
            redisTemplate.opsForValue().set("catelogJson", data, 1, TimeUnit.DAYS);
        } finally {
            redissonLock.unlock();
        }
        return result;
    }

    /**
     * 从数据库查询并封装我们分类数据
     **/
    public Map<String, List<Catelog2Vo>> getCatelogJsonFromDb() {

        /**
         * 分布式锁,Redis实现
         * 加锁成功了才能实现从数据库查询数据
         * 加锁必须使用原子命令,设置过期时间,防止出现死锁
         **/
        String uuid = UUID.randomUUID().toString();
        Boolean absent = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 30, TimeUnit.SECONDS);
        if (absent) {
            try {
                /**
                 * 只要是同一把锁,就能锁住需要这个锁的所有线程
                 * 线程得到锁之后,还要去Redis查询一下数据,如果有就直接返回,没有了,再去查询数据库
                 **/
                String catelogJson = redisTemplate.opsForValue().get("catelogJson");
                if (StringUtils.isEmpty(catelogJson)) {
                    /**
                     * 将数据库的多次查询变为一次
                     **/
                    List<CategoryEntity> categoryEntityList = baseMapper.selectList(null);

                    //查询所有的以及分类ID
                    List<CategoryEntity> level1Categorys = getParent_cid(categoryEntityList, 0L);
                    Map<String, List<Catelog2Vo>> parentVo = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
                        List<CategoryEntity> selectList = getParent_cid(categoryEntityList, v.getCatId());

                        //封装上面的结果
                        List<Catelog2Vo> collect = null;
                        if (!CollectionUtils.isEmpty(selectList)) {
                            collect = selectList.stream().map(l2 -> {
                                Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
                                //找出当前二级分类的三级分类数据封装成vo
                                List<CategoryEntity> parentId = getParent_cid(categoryEntityList, l2.getCatId());
                                if (!CollectionUtils.isEmpty(parentId)) {
                                    List<Catelog2Vo.Catelog3Vo> voList = parentId.stream().map(l3 -> {
                                        //封装成指定格式
                                        Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                                        return catelog3Vo;
                                    }).collect(Collectors.toList());
                                    catelog2Vo.setCatelog3list(voList);
                                }

                                return catelog2Vo;
                            }).collect(Collectors.toList());
                        }
                        return collect;
                    }));
                    String data = JSON.toJSONString(parentVo);
                    redisTemplate.opsForValue().set("catelogJson", data, 1, TimeUnit.DAYS);
                    return parentVo;
                }
                Map<String, List<Catelog2Vo>> result = JSON.parseObject(catelogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
                });
                //将数据转成json格式，并存入Redis中
                String data = JSON.toJSONString(result);
                redisTemplate.opsForValue().set("catelogJson", data, 1, TimeUnit.DAYS);
                return result;
            } finally {
                //执行完业务,一定要删除锁,删除锁的时候一定要判断是不是自己的锁,是自己的锁才能删除,s删除锁的时候也一定要保证删除操作是原子性的,所以需要用lua脚本来进行删除
                String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1] else return 0 end";
                Long deleteResult = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);

            /*String lock = redisTemplate.opsForValue().get("lock");
            if (uuid.equals(lock)){
                redisTemplate.delete("lock");
            }*/
            }
        } else {
            //获取锁失败,重试
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return getCatelogJsonFromDb();
        }
    }

    public List<CategoryEntity> getParent_cid(List<CategoryEntity> categoryEntityList, Long parent_cid) {
        /*return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", v.getCatId()));*/
        List<CategoryEntity> collect = categoryEntityList.stream().filter(item -> item.getParentCid().equals(parent_cid)).collect(Collectors.toList());
        return collect;
    }
}